<?php

/**
 * Large number of general purpose static methods
 */

if (!defined('AC_PHP_VERSION_MAJOR') && !defined('AC_PHP_VERSION_MINOR')) {
    $__phpversion = explode('.', PHP_VERSION, 2);
    define('AC_PHP_VERSION_MAJOR', intval($__phpversion[0]));
    define('AC_PHP_VERSION_MINOR', floatval($__phpversion[1]));
    unset($__phpversion);
}

if (!defined('AC_UTIL_DEFAULT_CHARSET')) define('AC_UTIL_DEFAULT_CHARSET', 'utf-8');

abstract class Ac_Util {

    protected static $autoLoadRegistered = null;
    
    /**
     * Adds one or more include paths
     * @param false|string|array $path Path(s) to add (FALSE means directory with 'classes' where current file resides)
     * @param bool $prepend Add this path to beginning of include_path list (not the end)
     */
    static function addIncludePath($path = false, $prepend = false) {
        if ($path === false) $path = dirname(dirname(__FILE__));
        $paths = explode(PATH_SEPARATOR, ini_get('include_path'));
        if (!is_array($path)) $path = array($path);
        if ($prepend) $paths = array_merge($path, array_diff($paths, $path));
            else $paths = array_merge(array_diff($paths, $path), $path);
        ini_set('include_path', implode(PATH_SEPARATOR, $paths)); 
    }

    static function getSafeIncludePath() {        
        $bd = explode(PATH_SEPARATOR, ini_get('open_basedir'));
        $p = explode(PATH_SEPARATOR, ini_get('include_path'));
        if ($bd) {
            foreach ($p as $i => $dir) {
                $found = false;
                foreach ($bd as $dir2) {
                    if (!strncmp($dir, $dir2, strlen($dir2))) {
                        $found = true;
                        break;
                    } 
                }
                if (!$found) unset($p[$i]);
            }
        }
        return $p;
    } 
    
    static function loadClass($className) {        
        if (!class_exists($className, false)) { // New behavior - use relative path to classDir
            $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $className);
            $fileName = str_replace('_', DIRECTORY_SEPARATOR, $fileName).'.php';
            $classDir = dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR;
            $f = $classDir.$fileName;
            $fileLoaded = false;
            if (is_file($f)) {
                require($f);
                $fileLoaded = true;
            } else {
                $p = self::getSafeIncludePath();
                foreach ($p as $dir) if (is_file($f = $dir.DIRECTORY_SEPARATOR.$fileName)) {
                    require($f);
                    $fileLoaded = true;
                    break;
                }
            }
            //if ($fileLoaded && !class_exists($className) || interface_exists($className))
            //    trigger_error (__FILE__."::".__FUNCTION__." - class '$className' not found in the $fileName", E_USER_ERROR);
        }
    }
    
    static function registerAutoload() {
        $res = false;
        if (self::$autoLoadRegistered === null) {
            if (function_exists('spl_autoload_register')) {
                $f = spl_autoload_functions();
                $cb = array('Ac_Util', 'loadClass');
                if (!is_array($f) || !in_array($cb, $f)) { 
                    spl_autoload_register($cb);
                    $res = true;                
                    if (function_exists('__autoload')) {
                        spl_autoload_register('__autoload');
                    }
                }
            } elseif (!function_exists('__autoload')) {

                function __autoload($className) {
                    return self::loadClass($className);
                }
                $res = true;

            }
            self::$autoLoadRegistered = $res;
        } else {
            $res = self::$autoLoadRegistered;
        }
        return $res;
    }
    
    static function implementsInterface($classOrObject, $interfaceName) {
        static $cache = array();
        
        $res = false;
        if (!is_object($classOrObject)) {
            if (class_exists($classOrObject, true) && interface_exists($interfaceName, true)) {
                if (!isset($cache[$classOrObject])) {
                    $r = new ReflectionClass($classOrObject);
                    $cache[$classOrObject] = $r->getInterfaceNames();
                }
                $res = in_array($interfaceName, $cache[$classOrObject]);
            }
        } else {
            $res = $classOrObject instanceof $interfaceName;
        }
        return $res;
    }
    
    static function m($paArray1, $paArray2, $preserveNumeric = false) {
        if (!is_array($paArray1) || !is_array($paArray2)) { return $paArray2; }
        if (!$paArray1) return $paArray2;
        if (!$paArray2) return $paArray1;
        foreach ($paArray2 AS $sKey2 => $sValue2) {
            if (is_int($sKey2) && !$preserveNumeric) {
                $paArray1[] = $sValue2;
            }
            else {
                if (!isset($paArray1[$sKey2])) $paArray1[$sKey2] = null;
                $paArray1[$sKey2] = self::m($paArray1[$sKey2], $sValue2, $preserveNumeric);
            }
        }
        return $paArray1;
    }

    static function ms(& $paArray1, $paArray2, $preserveNumeric = false) {
        return $paArray1 = self::m($paArray1, $paArray2, $preserveNumeric);
    }

    static function array_merge_recursive2($paArray1, $paArray2, $preserveNumeric  = false) {
        return self::m($paArray1, $paArray2, $preserveNumeric);
    }
    
    static function mkAttribs ($attribs = array(), $quote='"', $quoteStyle = ENT_QUOTES, $charset = false, $doubleEncode = true, $addSpace = true) {
        if (!$attribs) return "";
        if (isset($attribs['style']) && is_array($attribs['style'])) {
            $style = array();
            foreach ($attribs['style'] as $k => $v) $style[] = $k.": ".$v;
            $attribs['style'] = implode('; ', $style);
        }
        if ($charset === false) $charset = AC_UTIL_DEFAULT_CHARSET;
        $res = array();
        foreach ($attribs as $k => $v) {
            if (is_bool($v)) {
                if (!$v) continue;
                else $v = $k;
            }
            $res[] = $k."=".$quote.self::htmlspecialchars($v, $quoteStyle, $charset, $doubleEncode).$quote;
        }
        if ($res && $addSpace) array_unshift ($res, '');
        return implode(" ", $res);
    }
    
    static function mkElement($tagName, $tagBody = false, $attribs = array(), $quote='"', $quoteStyle = ENT_QUOTES, $charset = false, $doubleEncode = true) {
        $res = '<'.$tagName;
        if ($attribs) $res .= self::mkAttribs($attribs, $quote, $quoteStyle, $charset, $doubleEncode = true);
        if ($tagBody !== false) $res .= '>'.$tagBody.'</'.$tagName.'>';
            else $res .= ' />';
        return $res;
    }
    
    static function getEmailRx() {
        return Ac_Mail_Util::getEmailRegex();
    }

    /**
     * Can be used to obfuscate emails and any text - generates javascript that writes it
     * 
     * @param type $str
     * @return string 
     */
    static function jsEncode($str) {
        $rnd = rand(1, 1000);
        $res = "";
        $len = strlen($str);
        for ($i=$len-1; $i >= 0; $i--) 
            $res .= (ord($str{$i}) + $rnd).($i > 0? "," : "");
        $jsRes = "<script type='text/javascript'>";
            $jsRes .= "var a = new Array($res);";
            $jsRes .= "for (var i = a.length; i >= 0; i--) {";
                $jsRes .= "document.write(String.fromCharCode(a[i] - $rnd));";
            $jsRes .= "}";
        $jsRes .= "</script>";
        return $jsRes;
    }

    static function implodeRecursive($glue, $array, $array_name = NULL) {
        if (is_string($array)) return $array;
        if (is_array($glue)) {
            $impl = array_shift($glue);
            if (!$glue) $glue = $impl;
        } else $impl = $glue;
        $return = array();
        while(list($key,$value) = each($array)) {
            if(is_array($value)) $return[] = self::implodeRecursive($glue, $value, (string) $key);
            else 
                $return[] = $value;
        }
        return(is_array($return)? implode($impl, $return) : $return);
    }

    /**
     * @deprecated
     */
    static function implode_r($glue, $array, $array_name = NULL) {
        return self::implodeRecursive($glue, $array, $array_name);
    }
    
    static function getCsvLine($line,$delimiter=",",$enclosure="\"") {
        // Build the string
        $string = "";
         
        $writeDelimiter = FALSE;
        
        foreach($line as $dataElement){ 
            $dataElement=str_replace($enclosure, $enclosure.$enclosure, $dataElement);
            if($writeDelimiter) $string .= $delimiter;
            $string .= $enclosure . $dataElement . $enclosure;
            $writeDelimiter = TRUE;
        }
        $string .= "\n";
        
        return $string;
    }

    static function date ($src, $format = null, $useGmt = false, & $wasZeroDate = false) {
        return Ac_Model_DateTime::date($src, $format, $useGmt, $wasZeroDate);
    }

    static function createDirPath($dirPath, $rights = 0777) {
        $dirs = explode('/', $dirPath);
        $dir='';
        foreach ($dirs as $part) {
            $dir.=$part.'/';
            if (!is_dir($dir) && strlen($dir)>0)
                if (!mkdir($dir, $rights)) return false;
        }
        return true;
    }
    
    static function createFilePath($filePath, $rights = 0777) {
        $dir = dirname($filePath);
        self::createDirPath($dir, $rights);
        $handle = fopen($filePath, "w");
        return $handle;
    }

    static function listDirContents($dirPath, $recursive = false, $files = array(), $fileRegex = false, $dirRegex = false) {
        if(!($res = opendir($dirPath))) trigger_error("$dirPath doesn't exist!", E_USER_ERROR);
        while($file = readdir($res)) {
            if($file != "." && $file != "..") {
                if($recursive && is_dir("$dirPath/$file") && (!$dirRegex || preg_match($dirRegex, "$dirPath/$file"))) 
                    $files=self::listDirContents("$dirPath/$file", $recursive, $files, $fileRegex, $dirRegex);
                else {
                    if (!$fileRegex || preg_match($fileRegex, "$dirPath/$file")) {
                        array_push($files,"$dirPath/$file");
                    }
                } 
            }
        }
        
        closedir($res);
        
        return $files;
    }

    /**
     * Transforms foo[bar][baz] into array('foo','bar','baz')
     */
    static function pathToArray($path) {
        if (is_array($path)) return $path;
        if (!strlen($path)) return array();
        if (strpos($path, '[') === false) return array($path);
        return explode('[', str_replace(']', '', $path));
    }
    
    /**
     * Transforms array('foo', 'bar', 'baz') into foo[bar][baz]
     */
    static function arrayToPath($array) {
        if ($c = count($array)) {
            $res = $array[0];
            for ($i = 1; $i < $c; $i++) $res .= '['.$array[$i].']';
        } else $res = '';
        return $res;
    }
    
    /**
     * Splits array path 'foo[bar][baz]' into head ('foo') and tail ('bar[baz]')
     * @param string $path array path to split 
     * @return array (head, tail) tail is '' when no tail ;-))
     */
    static function pathHeadTail ($path) {
        if (!strlen($path)) return array('', '');
        if (($pos = strpos($path, '[')) === false) return array($path, '');
        $array = explode('[', str_replace(']', '', $path));
        $head = substr($path, 0, $pos);
        list ($head, $tail) = explode('[', $path, 2);
        $tail = implode('', explode(']', $tail, 2));
        return array($head, $tail);
    }
    
    /**
     * Splits array path 'foo[bar][baz]' into body ('foo[bar]') and tail ('baz')
     * @param string $path array path to split 
     * @return array (body, tail) body is '' when only one segment is present
     */
    static function pathBodyTail ($path) {
        $array = array_reverse(self::pathToArray($path));
        if ($c = count($array)) {
            $tail = $array[$c - 1];
            $body = self::arrayToPath(array_slice($array, 0, $c - 1));    
        } else $body = $tail = '';
        return array($body, $tail);
    }
    
    static function concatPaths ($path1, $path2) {
        if (!strlen($path2)) return $path1;
        if (!strlen($path1)) return $path2;
        $arr2 = self::pathToArray($path2);
        $res = $path1.'['.implode('][', $arr2).']';
        return $res; 
    }
    
    static function concatManyPaths ($path1, $path2) {
        $res = array();
        if (is_array($path1)) {
            foreach ($path1 as $p1) $res = array_merge($res, self::concatManyPaths($p1, $path2));        
        } 
        elseif (is_array($path2)) {
            foreach ($path2 as $p2) $res = array_merge($res, self::concatManyPaths($path1, $p2));
        } else {
            $res[] = self::concatPaths($path1, $path2);
        }
        return $res;
    }
    
    static function getObjectProperty($object, $property, $defaultValue = false, $treatArraysAsObjects = false) {
        $res = Ac_Accessor::getObjectProperty($object, $property, $defaultValue, $treatArraysAsObjects);
        return $res;
    }

    static function stripSlashes($value ) {
        $res = '';
        if (is_string( $value )) {
            $res = stripslashes( $value );
        } else {
            if (is_array( $value )) {
                $res = array();
                    foreach ($value as $key => $val) {
                        $res[$key] = self::stripSlashes( $val );
                    }
                } else {
                    $res = $value;
            }
        }
        return $res;
    }
    
    static function bindArrayToObject ($array, $obj, $ignore='', $prefix=NULL, $checkSlashes=false) {
        if (!is_array($array) || !is_object($obj)) return (false);
        if (!is_array($ignore)) $ignore = explode(" ", $ignore);
        $gmq = get_magic_quotes_gpc();
        foreach (array_keys(get_object_vars($obj)) as $k) {
            if ($k{0} != '_') {
                if (!in_array($k, $ignore)) {
                    if ($prefix) {
                        $ak = $prefix . $k;
                    } else {
                        $ak = $k;
                    }
                    if (isset($array[$ak])) {
                        $obj->$k = ($checkSlashes && $gmq) ? self::stripSlashes($array[$ak]) : $array[$ak];
                    }
                }
            }
        }
        return true;
    }
    
    static function simpleBind ($array, $obj) {
        foreach (array_keys($array) as $k) if (($k{0} !== '_') && isset($obj->$k)) {
            $obj->$k = $array[$k];
        }
    }
    
    static function simpleBindAll ($array, $obj, $onlyKeys = false) {
        if (is_array($onlyKeys)) $keys = array_intersect(array_keys($array), $onlyKeys);
            else $keys = array_keys($array);
        foreach ($keys as $k) if (($k{0} !== '_')) {
            $obj->$k = $array[$k];
        }
    }
    
    static function simpleBindVeryAll ($array, $obj, $onlyKeys = false) {
        if (is_array($onlyKeys)) $keys = array_intersect(array_keys($array), $onlyKeys);
            else $keys = array_keys($array);
        foreach ($keys as $k) {
            $obj->$k = $array[$k];
        }
    }
    
    static function smartBind ($array, $obj) {
        foreach (array_keys($array) as $k) if ($k{0} !== '_') {
            if (method_exists($obj, $setter = 'set'.$k)) $obj->$setter($array[$k]);
            elseif (isset($obj->$k)) $obj->$k = $array[$k];
        }
    }
    
    static function filterValue($value, $noTrim = false, $allowHtml = false) {
        if (!is_scalar($value)) return false;
        if (get_magic_quotes_gpc()) $value = stripslashes($value);
        if (!$allowHtml) $value = strip_tags($value);
        if (!$noTrim) $value = trim($value);
        return $value;
    }
    
    /**
     * Returns item from nested arrays using specified set of keys.
     * 
     * If $path is array('foo', 'bar'), $arr['foo']['bar'] will be returned. 
     * 
     * @param array $arr           Array with information source
     * @param string|array $path  Keys that we are interested in (string 'path' will be converted to array('path'))
     * @param mixed $defaultValue Value that will be returned when corresponding entry is not found
     */
    static function getArrayByPath($arr, $arrPath, $defaultValue = null, & $found = false) {
        if (!is_array($arrPath)) $arrPath = array($arrPath);
        $src = $arr;
        $arrPath = array_reverse($arrPath);
        while ($arrPath) {
            $key = array_pop($arrPath);
            if (is_array($src) && array_key_exists($key, $src)) $src = $src[$key];
                else {
                    $found = false;
                    return $defaultValue;
                }
        }
        $found = true;
        return $src;
    }
    
    /**
     * Synonim for getArrayByPath that also automatically converts string paths to arrays
     * 
     * @param array $arr Source array
     * @param string|array $path Path in array
     * @param mixed $defaultValue Value to return if any of path keys isn't found
     * 
     * @return mixed
     */
    static function gap(array $arr, $path, $defaultValue = null) {
        if (!is_array($path)) $path = self::pathToArray($path);
        return self::getArrayByPath($arr, $path, $defaultValue);
    }
    
    /**
     * Sets element of nested arrays using specified set of keys.
     * 
     * If $arrPath is array('foo', 'bar'), $arr['foo']['bar'] will be set to $value. 
     * 
     * @param array $arr             Array which we are going to modify
     * @param string|array $arrPath      Keys that we are interested in (string 'path' will be converted to array('path'))
     * @param mixed $value           Value that we want to set
     * @param bool|callback $overwriteNonArrays  If one of mid-segments of path points to non-array, how conflict should be resolved
     * @param array $extraParams    Extra params to pass to callback static function
     * @return bool              Whether operation succeeded or not (depends on $overwrite and current $arr values)
     * 
     * Example of how $overwriteNonArrays works:
     * <code>
     *      $arr = array('x' => array('x1' => 'valX1', 'x2' => 'valX2'));
     *      $arrPath = array('x', 'x1', 'x11');
     *      $overwrite = false;
     *      var_dump(self::setArrayByPath($arr, $arrPath, 'foo', $overwrite)); // Will return false; $arr will remain unchanged
     * 
     *      $arr = array('x' => array('x1' => 'valX1', 'x2' => 'valX2'));
     *      $arrPath = array('x', 'x1', 'x11');
     *      $overwrite = false;
     *      var_dump(self::setArrayByPath($arr, $arrPath, 'foo', $overwrite)); // Will return true; 
     *      var_dump($arr); // array('x' => array('x1' => array('x11' => 'foo'), 'x2' => 'valX2'));
     * 
     *      static function overwriteCallback($currPath, & $element, $value) {
     *          if ($element == 'valX1') {
     *              $element = array($element);
     *          } elseif ($element == 'valX2') {
     *              return false;   
     *          }
     *      }
     * 
     *      $arr = array('x' => array('x1' => 'valX1', 'x2' => 'valX2'));
     *      $overwrite = 'overwriteCallback';
     *      var_dump(self::setArrayByPath($arr, array('x', 'x1', 'x11'), 'foo', $overwrite)); // Will return true
     *      var_dump($arr); // array('x' => array('x1' => array('valX1', 'x11' => 'foo')), 'x2' => 'valX2');
     * 
     *      var_dump(self::setArrayByPath($arr, array('x2', 'x21'), 'foo', $overwrite)); // Will return false; $arr will remain unchanged
     * </code> 
     */
    static function setArrayByPath(& $arr, $arrPath, $value, $overwrite = false, $extraParams = array()) {
        if (!is_array($arrPath)) $arrPath = array($arrPath);
        $src = & $arr;
        if ($arrPath) {
            $key = array_shift($arrPath);
            while ($arrPath) {
                if (!isset($src[$key])) $src[$key] = array();
                elseif (!is_array($src[$key])) {
                    if ($overwrite === false) return false;
                    elseif ($overwrite === true) $src[$key] = array();
                    elseif (is_callable($overwrite)) {
                        $oRes = call_user_func_array($overwrite, array_merge(array(array_reverse(array_merge($arrPath, array($key))), & $src[$key], $value), $extraParams));
                        if ($oRes === false) return false;
                        if (!is_array($src[$key])) $src[$key] = array();
                    }
                }
                $src = & $src[$key];
                $key = array_shift($arrPath);
            }
            $src[$key] = $value;
        } else {
            $src = $value;
        }
        return true;
    }

    /**
     * Unsets item of nested arrays using specified set of keys.
     * 
     * If $path is array('foo', 'bar'), $arr['foo']['bar'] will be unset. 
     * 
     * @param array $arr           Array with information source
     * @param string|array $path  Keys of element that we want to unset (string 'path' will be converted to array('path'))
     * @return bool                Whether we have found (and unset) our element, or not 
     */
    static function unsetArrayByPath(& $arr, $arrPath) {
        if (!is_array($arrPath)) $arrPath = array($arrPath);
        $src = & $arr;
        while ($arrPath) {
            $key = array_shift($arrPath);
            if (is_array($src) && isset($src[$key])) {
                if ($arrPath) $src = & $src[$key];
                else {
                    unset($src[$key]);                  
                }
            } else return false;
        }
        return true;
    }
    
    static function simpleSetArrayByPath(& $arr, $arrPath, & $value, $unique = true, $noRef = false) {
        if (!is_array($arrPath)) $arrPath = array($arrPath);
        $src = & $arr;
        $arrPath = array_reverse($arrPath);
        if (count($arrPath) && !strlen($arrPath[0])) {
            $unique = false;
            array_splice($arrPath, 0, 1);
        }
        $key = array_pop($arrPath);
        while ($arrPath) {
            if (!strlen($key)) {
                if (!count($src)) $key = 0;
                else $key = max(0, (int) max(array_keys($src)));
            }
            if (!isset($src[$key])) $src[$key] = array();
            elseif (!is_array($src[$key])) {
                $src[$key] = array();
            }
            $src = & $src[$key];
            $key = array_pop($arrPath);
        }

        if ($unique) $src[$key] = & $value;        
            else $src[$key][] = & $value;
    }
    
    static function simpleSetArrayByPathNoRef(& $arr, $arrPath, $value, $unique = true) {
    	return self::simpleSetArrayByPath($arr, $arrPath, $value, $unique);
    }
    
    static function & simpleGetArrayByPath($arr, $arrPath, $defaultValue = null) {
        if (!is_array($arrPath)) $arrPath = array($arrPath);
        $src = & $arr;
        $arrPath = array_reverse($arrPath);
        while ($arrPath) {
            $key = array_pop($arrPath);
            if (is_array($src) && isset($src[$key])) $src = & $src[$key];
                else return $defaultValue;
        }
        return $src;
    }
    
    static function getIntArray($varName, $srcArr) {
        if (!isset($srcArr[$varName])) return array();
        $res = array();
        if (!is_array($srcArr[$varName])) $s = array($srcArr[$varName]); else $s = $srcArr[$varName];
        foreach ($s as $v) {
            if (is_numeric($v)) $res[] = (int) $v;
        }
        return $res;
    }

    static function makeCsvLine($line,$delimiter=";",$enclosure="\"", $addNewLine = true, $forceText = false) {
        $string = "";
        $writeDelimiter = FALSE;
        foreach($line as $dataElement){
            $dataElement = str_replace($enclosure, $enclosure.$enclosure, $dataElement);
            if($writeDelimiter) $string .= $delimiter;
            if ($forceText && (strpos($dataElement, $delimiter) === false)) $string .= '='; 
            $string .= $enclosure . $dataElement . $enclosure;
            $writeDelimiter = TRUE;
        }
        if ($addNewLine) $string .= "\n";
        return $string;
    }
    
    static function d($s) {
        echo '<pre>'.htmlspecialchars(print_r($s, 1)).'</pre>';
    }
    
    static function array_values($array) {
        foreach ($array as $v) $res[] = $v;
        return $res;
    }
    
    /**
     * @param array $keys list of keys of return array
     * @return array ('key1' => false, 'key2' => false...)
     */
    static function lazyArray($keys) {
        foreach ($keys as $k) $res[$k] = false;
        return $res;
    }
    
    static function stripTrailingSlash($string, $slash = '/\\') {
        return self::removeTrailingSlash($string, $slash);
    }
    
    static function flattenArray($array, $level = -1, $keyGlue = false, $key = '') {
        $res = array();
        foreach ($array as $k => $v) {
            if (strlen($keyGlue)) {
                $tk = strlen($key)? $key.$keyGlue.$k : $k;
            } else {
                $tk = false;
            }
            if (is_array($v) && ($level != 0)) $res = array_merge($res, self::flattenArray($v, $level-1, $keyGlue, $tk));
            elseif (strlen($tk)) {
                $res[$tk] = & $array[$k];
            } else {
                $res[] = & $array[$k];
            }
        }
        return $res;
    }
    
    /**
     * @return bool True if $object1 and $object2 are both references to the same object
     */
    static function sameObject(& $object1, & $object2) {
        return $object1 === $object2;
    }
    
    static function array_unique($arr, $strict = false) {
        $res = array();
        foreach ($arr as $i) if (!in_array($i, $res, $strict)) $res[] = $i;
        return $res;
    }

    /**
     * Converts 'foo_bar' to 'Foo_Bar'
     */
    static function fixClassName($className) {
        /*$className = str_replace("_", " ", $className);
        $className = ucwords($className);
        $className = str_replace(" ", "_", $className);*/
        return $className;
    }
    
    static function sameInArray(& $obj, $array) {
        $res = false;
        foreach(array_keys($array) as $k) {
            if (self::sameObject($obj, $array[$k])) {
                $res = true;
                break;
            }
        }
        return $res;
    }

    static function bindAutoparams($obj, $options, $alsoSimpleBind = true, $firstOnes = false) {
        $bind = true;
        if (method_exists($obj, 'bindAutoparams')) { 
            if ($obj->bindAutoparams($options, $alsoSimpleBind, $firstOnes) === false) $bind = false;
        }
        if ($bind) {
            $ov = get_object_vars($obj);
            $keys = array_keys($options);
            if (is_array($firstOnes)) {
                $keys = array_unique(array_merge(array_intersect($firstOnes, $keys), $keys));
            }
            foreach ($keys as $k) {
                if (is_callable($call = array($obj, 'set'.ucfirst($k))) || is_callable($call = array($obj, '_set'.ucfirst($k)))) { 
                    call_user_func($call, $options[$k]);
                }
                elseif ($alsoSimpleBind && ($k{0} !== '_') && array_key_exists ($k, $ov)) $obj->{$k} = $options[$k]; 
            }
        }
    }
    
    static function factoryWithOptions ($options = array(), $baseClass, $classParam = 'class', $ensureBaseClass = true, $loadWithUnderscores = false) {
        $className = $baseClass;
        if (strlen($classParam) && isset($options[$classParam]) && strlen($options[$classParam])) 
            $className = $options[$classParam];
        if (!class_exists($className)) {
        	if ($loadWithUnderscores) {
        		require($fName = str_replace('_', DIRECTORY_SEPARATOR, trim($className,'_').'.php'));
        		if (!class_exists($className)) trigger_error ("Class '$className' not found in '{$fName}'", E_USER_ERROR);
        	}
        	trigger_error ("Class '$className' not found", E_USER_ERROR);
        }
        $res = new $className ($options);
        if ($ensureBaseClass && !is_a($res, $baseClass)) trigger_error("Class '{$className}' is not a descendant of '{$baseClass}'", E_USER_ERROR);
        return $res;
    }
    
    static function getUploadedFilesByHierarchy() {
        $newOrdering = array();
        if (isset($_FILES)) {
            foreach ($_FILES as $var => $info) {
                foreach (array_keys($info) as $attr) {
                    self::groupFileInfoByVariable($newOrdering[$var], $info[$attr], $attr);
                }
            }
        }
        return $newOrdering;
    }

    protected static function groupFileInfoByVariable(&$top, $info, $attr) {
       if (is_array($info)) {
           foreach ($info as $var => $val) {
               if (is_array($val)) {
                   self::_groupFileInfoByVariable($top[$var], $val, $attr);
               } else {
                   $top[$var][$attr] = $val;
               }
           }
       } else {
           $top[$attr] = $info;
       }
       return true;
    }
    
    static function showCoolTable($tableData, $colTitles, $rowTitles, $defaultRow = array(), $returnCells = false) {
        $colsList = array_unique(array_merge(array_keys($colTitles), array_keys($defaultRow)));
        foreach (array_keys($colTitles) as $k) if (!isset($defaultRow[$k])) $defaultRow[$k] = '';
        foreach ($tableData as $k => $row) {
            $colsList = array_unique(array_merge ($colsList, array_keys($row)));
            $tableData[$k] = array_merge($defaultRow, $row);
        }
        $cells = array();
        $headerRow = array('');
        foreach ($colsList as $col) {
            $headerRow[] = isset($colTitles[$col])? $colTitles[$col] : '';
        }
        $cells[] = $headerRow;
        foreach ($tableData as $k => $row) {
            $rowCells = array();
            if (isset($rowTitles[$k])) $rowCells[] = $rowTitles[$k]; else $rowCells[] = '';
            foreach ($colsList as $col) $rowCells[] = isset($row[$col])? $row[$col] : '';
            $cells[] = $rowCells;
        }
        if ($returnCells) return $cells;
        echo '<table>'; foreach ($cells as $cellRow) {
            echo '<tr>'; foreach ($cellRow as $cell) {
                if (substr($cell, 0, 3) != '<td') echo '<td>'; 
                echo strlen(trim($cell))? $cell : '&nbsp;';
                if (substr($cell, 0, 3) != '<td') echo '</td>';
            }
            echo '</tr>'; 
        }
        echo '</table>';
    }
    
    static function getClassVars($className) {
        if (is_object($className)) $className = get_class($className);
        return get_class_vars($className);
    }
    
    /**
     * @param object $object
     * @return array (property => value)
     */
    static function getPublicVars($object) {
        // Workaround to change scope to skip using Reflection
        $g = new _Ae_Util_ObjectVarGetter();
        return $g->getObjectVars($object);
    }
    
    /**
     * @param object $object
     * @return array
     */
    static function getPublicMethods($object) {
        // Workaround to change scope to skip using Reflection
        $g = new _Ae_Util_ObjectVarGetter();
        return $g->getObjectMethods($object);
    }
    
    static function isMethodOverridden($class, $baseClass, $method = '') {
        static $cache = array();
        $cid = "{$class}/{$baseClass}/{$method}";
        if (isset($cache[$cid])) return $cache[$cid];
        $refClass = new ReflectionClass($class);
        $allOverridden = array();
        foreach ($refClass->getMethods() as $m) {
            $methodName = $m->getName();
            $r = $m->getDeclaringClass()->getName();
            $isOverridden = ($r !== $baseClass);
            $cache["{$class}/{$baseClass}/{$methodName}"] = $isOverridden;
            if ($isOverridden) $allOverridden[] = $methodName;
        }
        $cache["{$class}/{$baseClass}/"] = $allOverridden;
        if (!strlen($method)) $isOverridden = $allOverridden;
        if (!isset($cache[$cid])) {
            throw Ac_E_InvalidCall::noSuchMethod($class, $method);
        }
        $isOverridden = $cache[$cid];
        return $isOverridden;
    }
    
    static function htmlEntityDecode($stringOrArray, $quoteStyle = ENT_QUOTES, $charset = null) {
        if (is_array($stringOrArray)) {
            $res = array();
            foreach ($stringOrArray as $k => $v) $res[$k] = self::htmlEntityDecode($v, $quoteStyle, $charset);
            return $res;
        }
        return html_entity_decode($stringOrArray, $quoteStyle, $charset);
    }
        
    static function htmlspecialchars($string, $quote_style = ENT_QUOTES, $charset = null, $double_encode = true)
    {
        
        if (!strlen($charset)) $charset = null;
        if ($double_encode) return htmlspecialchars($string, $quote_style, $charset);
        elseif ((AC_PHP_VERSION_MAJOR >= 5) && (AC_PHP_VERSION_MINOR >= 2.3)) {
            return htmlspecialchars($string, $quote_style, $charset, $double_encode);
        }
        else {
            if (!$double_encode) {
                $res = htmlspecialchars($string, $quote_style, $charset);
                $res = preg_replace("/\&amp;((#[0-9][0-9]*)|(#[xX][0-9a-fA-F]+)|([a-z][0-9a-zA-Z]*));/",'&\\1;', $res);
                return $res;
            } else {
                return htmlspecialchars($string, $quote_style, $charset);
            }
        }
    }
    
    static function addTrailingSlash($string, $slashes = '/\\', $slashType = DIRECTORY_SEPARATOR) {
        $res = self::removeTrailingSlash($string, $slashes).$slashType;
        return $res;
    }
    
    static function addLeadingSlash($string, $slashes = '/\\', $slashType = DIRECTORY_SEPARATOR) {
        $res = $slashType.self::removeLeadingSlash($string, $slashes);
        return $res;
    }
    
    static function removeTrailingSlash($string, $slashes = '/\\') {
        $res = rtrim($string, $slashes);
        return $res;
    }
    
    static function removeLeadingSlash($string, $slashes = '/\\') {
        $res = ltrim($string, $slashes);
        return $res;
    }
    
    static function flatArray2tree ($flatArray, $keys, $lastLevelIsArray = true, $unsetKeys = false) {
    	$res = array();
    	if (!is_array($keys)) $keys = array($keys);
    	$unique = !$lastLevelIsArray;
    	foreach ($flatArray as $key => & $item) {
    		$path = array();
    		foreach ($keys as $k) {
    			if (is_null($k)) $path[] = $key;
    			elseif (isset($item[$k])) {
    				$path[] = $item[$k];
    				if ($unsetKeys) unset($item[$k]);
    			}
    		}
    		if (count($path)) self::simpleSetArrayByPath($res, $path, $item, $unique);
            }
    	return $res;
    }
    
    /**
     * Replaces objects in the array with the output of their $methodName() method.
     * DO NOT USE until it's finished 
     * 
     * @param mixed $src 							Data source. For objects their $methodName() value is returned; arrays are processed recursively and other values are returned as-is. 
     * @param string $methodName 					Method name to convert objects. 
     * @param array $arguments 						Arguments for called method (if any).
     * @param bool $descendIntoCollectedResults		If $src->$methodName() returns an array or an object, have collectRecursive() applied to the result before return 
     * @param bool $rejectNonCompatible				Remove objects that don't have $methodName() method from output (also return NULL if such object is provided as $src) 
     * @return mixed
     */
    static function & collectRecursive(& $src, $methodName, $arguments = array(), $descendIntoCollectedResults = true, $rejectNonCompatible = false) {
    	if (is_array($src)) {
    		$res = array();
    		foreach (array_keys($src) as $k) {
    			if (is_object($src[$k]) && (!$rejectNonCompatible || is_callable(array($src[$k], $methodName)))) {
    				$res[$k] = $src[$k];
    			}
    		}
    	} elseif (is_object($src)) {
    		//  NOTE: unfinished function!!! 
    	} else {
    		$res = $src;
    	}
    	return $res;
    }

    /**
     * Converts any non-array value into an array.
     * Array will contain one element (with index 0) if value is not NULL and not FALSE. NULL and FALSE are converted to an empty array.
     * Array parameters will be returned as-is.
     * 
     * @param mixed $something
     * @return array 
     */
    static function toArray($something) {
        if (!is_array($something)) {
            if (is_null($something) || $something === false) $res = array();
            else $res = array($something);
        } else $res = $something;
        return $res;
    }
    
    /**
     * Recursively filters array $arr leaving only objects of $requiredClasses
     * @param array $arr
     * @param string|array $allowedClasses One or several class names
     * @param bool $withEmptyArrays Whether to leave empty arrays (ones without objects) in result
     */
    static function getObjectsOfClass(array $arr, $requiredClasses, $withEmptyArrays = false) {
        $res = array();
        $c = self::toArray($requiredClasses);
        foreach ($arr as $k => $v) {
            if (is_object($v)) {
                foreach ($c as $class) 
                    if ($v instanceof $class) {
                        $res[$k] = $v;
                        break;
                    }
            } elseif (is_array($v)) {
                $v = self::getObjectsOfClass($v, $c, $withEmptyArrays);
                if ($withEmptyArrays || count($v)) $res[$k] = $v;
            }
        }
        return $res;
    }


    /**
     * This obfuscated function obfuscates email
     * 
     * Borrowed from http://www.maurits.vdschee.nl/php_hide_email/ (license: Public Domain)
     * 
     * @param string $email Email to obfuscate
     * @return string HTML+javascript code 
     */
    static function obfuscateEmail($email) {
        $character_set = '+-.0123456789@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
        $key = str_shuffle($character_set); $cipher_text = ''; $id = 'e'.rand(1,999999999);
        for ($i=0;$i<strlen($email);$i+=1) $cipher_text.= $key[strpos($character_set,$email[$i])];
        $script = 'var a="'.$key.'";var b=a.split("").sort().join("");var c="'.$cipher_text.'";var d="";';
        $script.= 'for(var e=0;e<c.length;e++)d+=b.charAt(a.indexOf(c.charAt(e)));';
        $script.= 'document.getElementById("'.$id.'").innerHTML="<a href=\\"mailto:"+d+"\\">"+d+"</a>"';
        $script = "eval(\"".str_replace(array("\\",'"'),array("\\\\",'\"'), $script)."\")"; 
        $script = '<script type="text/javascript">/*<![CDATA[*/'.$script.'/*]]>*/</script>';
        return '<span id="'.$id.'">[javascript protected email address]</span>'.$script;
    }
    
    static function stringifyObjects(array $array) {
        $res = array();
        foreach ($array as $k => $v) {
            if (is_object($v)) {
                if (method_exists($v, '__toString')) $v = ''.$v;
                else $v = '[object '.get_class($v).']';
            }
            elseif (is_array($v)) $v = self::stringifyObjects($v);
            $res[$k] = $v;
        }
        return $res;
    }
    
    static function typeClass($item) {
        $res = gettype($item);
        if (is_object($item)) $res .=  ' '.get_class($item);
        return $res;
    }
    
    static function getClassConstants($class, $prefix = false) {
        $r = new ReflectionClass($class);
        $res = $r->getConstants();
        if ($prefix !== false) {
            $l = strlen($prefix);
            foreach ($res as $k => $v) if (strncmp($k, $prefix, $l)) unset($res[$k]);
        }
        return $res;
    }
    
    /**
     * Indexes array by keys and optionally replaces values by subset of them
     * 
     * Ac_Util::indexArray([['foo' => 'a'], ['foo' => 'b']], 'foo', true) 
     *  will return ['a' => ['foo' => 'a'], 'b' => ['foo' => 'b']]
     * 
     * Ac_Util::indexArray([['foo' => 'a'], ['foo' => 'b']], 'foo', false) 
     *  will return ['a' => [['foo' => 'a']], 'b' => [['foo' => 'b']]]
     * 
     * Ac_Util::indexArray([['foo' => 'a', 'bar' => 'b'], ['foo' => 'c', 'bar' => 'd'], ['foo', 'bar'], true)
     *  will return ['a' => ['b' => ['foo' => 'a', 'bar' => 'b']], 'c' => ['d' => ['foo' => 'c', 'bar' => 'd']]]
     * 
     * Ac_Util::indexArray([['foo' => 'a', 'bar' => 'b'], ['foo' => 'c', 'bar' => 'd'], 'foo', true, 'bar')
     *  will return array('a' => 'b', 'c' => 'd')
     * 
     * Ac_Util::indexArray([
     *          ['foo' => 'a', 'bar' => 'b', 'baz' => 'c'], 
     *          ['foo' => 'd', 'bar' => 'e', 'baz' => 'f'], 
     *  ], 'foo', true, array('bar', 'baz')) 
     *  will return ['a' => ['bar' => 'b', 'baz' => 'c'], ['bar' => 'e', 'baz' => 'f']]
     * 
     * @param array $flatArray Original array of members: arrays or objects
     * @param scalar|array $keyList Keys to index array
     * @param bool unique whether last level will be exact member of original list or array of members
     * @param false|scalar|array $valueKeys if provided, members will be replaced by extracted keys' or
     *      properties' values before placing into result array (scalar $valueKeys means sclar result member, 
     *      array $valueKeys means always array result member)
     * 
     */
    static function indexArray(array $flatArray, $keyList, $unique = false, $valueKeys = false) {
        $res = array();
        if (is_array($keyList) && count($keyList) == 1) $keyList = array_shift($keyList);
        if (!is_array($keyList)) {
            // simple version
            foreach ($flatArray as $v) {
                if (is_object($v)) {
                    $k = Ac_Accessor::getObjectProperty($v, $keyList);
                }
                else {
                    $k = isset($v[$keyList])? $v[$keyList] : null;
                }
                if ($valueKeys !== false && !is_null($valueKeys))
                    $v = Ac_Accessor::getObjectProperty($v, $valueKeys, null, true);
                
                if ($unique) $res[$k] = $v;
                else $res[$k][] = $v;
            }
        } else {
            foreach ($flatArray as $v) {
                if (is_object($v)) {
                    $path = array_values(Ac_Accessor::getObjectProperty($v, $keyList));
                }
                else {
                    $path = array();
                    foreach ($keyList as $i) if (isset($v[$i])) $path[] = $v[$i];
                        else $path[] = null;
                }
                if ($valueKeys !== false && !is_null($valueKeys))
                    $v = Ac_Accessor::getObjectProperty($v, $valueKeys, null, true);
                Ac_Util::simpleSetArrayByPathNoRef ($res, $path, $v, $unique);
            }
        }
        return $res;
    }
    
    static function lcFirst($string) {
        if (strlen($string)) $string{0} = strtolower($string{0});
        return $string;
    }
    
}

class _Ae_Util_ObjectVarGetter {
    
    function getObjectVars($foo) {
        if (is_object($foo)) {
            return get_object_vars($foo);
        } else {
            return get_class_vars($foo);
        }
    }
    
    function getObjectMethods($foo) {
        if (is_object($foo)) $class = get_class($foo);
            else $class = $foo;
        return get_class_methods($class);
    }
    
}

/**
 * Used to register as unserialize handler (some version of PHP didn't allow to provide a static call)
 * @param type $className 
 */
function acUtilLoadClass($className) {
    self::loadClass(self::fixClassName($className));
}
